﻿namespace JoinBox.Basal;

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

/// <summary>
/// 键盘钩子
/// </summary>
public class KeyboardHook : IDisposable
{
    /// <summary>
    /// 键盘组合键同时按下
    /// </summary>
    public event KeyEventHandler? KeyDownEvent;
    /// <summary>
    /// 键盘按住
    /// </summary>
    public event KeyPressEventHandler? KeyPressEvent;
    /// <summary>
    /// 键盘弹起
    /// </summary>
    public event KeyEventHandler? KeyUpEvent;

    static int _hHook = 0; // 声明键盘钩子处理的初始值

    /// <summary>
    /// 钩子回调
    /// </summary>
    /// <remarks>
    /// 不要试图省略此变量,否则将会导致GC变量池满后释放<br/>
    /// 提示:激活 CallbackOnCollectedDelegate 托管调试助手(MDA)
    /// </remarks>
    readonly WindowsAPI.HookProc? _hookProc;


    /// <summary>
    /// 安装键盘钩子<br/>
    /// <a href="https://blog.csdn.net/u014434080/article/details/51513197">相关链接</a>
    /// </summary>
    /// <param name="IsSystemHook"><see cref="true"/>系统钩子;<see cref="false"/>线程钩子</param>
    public KeyboardHook(bool IsSystemHook = true)
    {
        if (_hHook != 0)
            return;

        // 回调函数
        _hookProc = new WindowsAPI.HookProc(KeyboardHookProc);

        if (IsSystemHook)
        {
            // 系统钩子
            var moduleName = WindowsAPI.GetModuleHandle(
                                System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName);
            _hHook = WindowsAPI.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, _hookProc, moduleName, 0);
        }
        else
        {
            // 线程钩子(cad08用会出错,见回调函数说明)
            _hHook = WindowsAPI.SetWindowsHookEx(HookType.WH_KEYBOARD,
                                _hookProc, IntPtr.Zero, WindowsAPI.GetCurrentThreadId());
        }

        if (_hHook == 0)
        {
            Dispose();
            throw new Exception("安装键盘钩子失败");
        }
    }

    /// <summary>
    /// 中断回调阻断输入
    /// </summary>
    public bool IsHookBreak = false;

    /// <summary>
    /// 回调函数
    /// </summary>
    private int KeyboardHookProc(int nCode, int wParam, IntPtr lParam)
    {
        IsHookBreak = false;
        // 侦听键盘事件
        if ((nCode >= 0) &&
            (KeyDownEvent is not null
            || KeyUpEvent is not null
            || KeyPressEvent is not null))
        {
            var wPa = (WM)wParam;

            // cad08线程钩子转换键值会变成以下的值,没弄懂..
            // 所以我用系统钩子再判断窗口焦点是cad才执行.GetForegroundWindow();
            // if (wPa == WM.WM_QUERYENDSESSION)// 按ctrl会传这个信息
            //     return CallNextHookEx(_hHook, nCode, wParam, lParam);

            var keyMSG = Marshal.PtrToStructure(lParam, typeof(WindowsAPI.KeyMSG))
                         as WindowsAPI.KeyMSG;

            if (keyMSG == null)
                throw new ArgumentException(nameof(keyMSG));

            // 键盘组合键同时按下
            if (KeyDownEvent is not null &&
                (wPa == WM.WM_KEYDOWN || wPa == WM.WM_SYSKEYDOWN))
            {
                var keyData = (Keys)keyMSG.vkCode;
                var e = new KeyEventArgs(keyData);
                KeyDownEvent(this, e);
            }

            // 键盘按下
            if (KeyPressEvent is not null &&
                wPa == WM.WM_KEYDOWN)
            {
                byte[] keyState = new byte[256];
                WindowsAPI.GetKeyboardState(keyState);

                byte[] inBuffer = new byte[2];
                if (WindowsAPI.ToAscii(
                    keyMSG.vkCode,
                    keyMSG.scanCode,
                    keyState,
                    inBuffer,
                    keyMSG.flags) == 1)
                    KeyPressEvent(this, new((char)inBuffer[0]));
            }

            // 键盘抬起
            if (KeyUpEvent is not null &&
                (wPa == WM.WM_KEYUP || wPa == WM.WM_SYSKEYUP))
                KeyUpEvent(this, new((Keys)keyMSG.vkCode));

            if (IsHookBreak)
                return 1;
        }

        /// 返回1不再传递
        /// 返回0或调用<see cref="WindowsAPI.CallNextHookEx"/>
        /// 函数则消息出了这个钩子继续往下传递,也就是传给消息真正的接受者
        return WindowsAPI.CallNextHookEx(_hHook, nCode, wParam, lParam);
    }



    #region IDisposable接口相关函数
    public bool IsDisposed { get; private set; } = false;

    /// <summary>
    /// 手动调用释放
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 析构函数调用释放
    /// </summary>
    ~KeyboardHook()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        // 不重复释放,并设置已经释放
        if (IsDisposed) return;
        IsDisposed = true;


        bool retKeyboard = true;
        if (_hHook != 0)
        {
            retKeyboard = WindowsAPI.UnhookWindowsHookEx(_hHook);
            _hHook = 0;
        }
        if (!retKeyboard)
            throw new Exception("Dispose键盘钩子失败!");
    }
    #endregion
}